package play.modules.googleclosure;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.plovr.*;
import play.Play;
import play.PlayPlugin;
import play.mvc.Http.Request;
import play.mvc.Http.Response;
import play.vfs.VirtualFile;

/**
 * Plugin to call plovr (www.plovr.com), a build tool for google closure tools, to compile javascript and soy files
 * author: Dallan Quass
 */
public class PlovrPlugin extends PlayPlugin {
   private Map<VirtualFile,Config> inputConfigs = null;
   private PlovrCompileRequestHandler compiler;
   private PlovrInputFileHandler fetcher;
   private String cacheFor = null;

   @Override
   public void onLoad() {
      String configFilename = Play.configuration.getProperty("plovr.conf");
      if (configFilename != null) {
         cacheFor = Play.configuration.getProperty("http.cacheControl", "3600")+"s"; // cacheFor requires ending d/h/mn/s
         int port = Integer.parseInt(Play.configuration.getProperty("http.port", "9000"));
         Config config;
         try {
            config = ConfigParser.parseFile(new File(configFilename));
            CompilationServer server = new CompilationServer("", port); // address isn't used, but port is
            server.registerConfig(config);
            compiler = new PlovrCompileRequestHandler(server);
            fetcher = new PlovrInputFileHandler(server);
            inputConfigs = new HashMap<VirtualFile,Config>();
            for (JsInput jsInput : config.getManifest().getRequiredInputs()) {
               VirtualFile vf = Play.getVirtualFile(jsInput.getName());
               inputConfigs.put(vf, config);
               if (Play.mode != Play.Mode.DEV && config.getCompilationMode() != CompilationMode.RAW) {
                  // pre-compile
                  Request request = new Request(); // request can be empty; it's only used in RAW mode as far as I can tell
                  compileAndCache(vf, request, config);
               }
            }
         } catch (IOException e) {
            play.Logger.fatal("Error reading config file: "+configFilename);
         }
      }
   }

   @Override
   public boolean serveStatic(VirtualFile file, Request request, Response response) {
      Config config;
      // if configuration contains plovr.conf and plovr.conf file has the specified file as an input file
      if (inputConfigs != null && file.getName().endsWith(".js") && (config = inputConfigs.get(file)) != null) {
         try {
            // Cache compiled files
            play.Logger.trace("play-plovr serveStatic="+file.relativePath());
            String js = compileAndCache(file, request, config);
            response.contentType = "text/javascript";
            response.status = 200;
            if(Play.mode == Play.Mode.PROD) {
               response.cacheFor(cacheFor);
            }
            response.print(js);
         } catch(Exception e) {
            response.contentType = "text/javascript";
            response.status = 500;
            response.print("Oops,\n");
            e.printStackTrace(new PrintStream(response.out));
         }
         return true;
      }

      return super.serveStatic(file, request, response);
   }

   private String compileAndCache(VirtualFile file, Request request, Config config) throws IOException {
      String js = null;
      CachedJS cachedJS = cache.get(file.relativePath());
      if (cachedJS != null) {
         if (Play.mode != Play.Mode.DEV || cachedJS.isStillValid()) {
            js = cachedJS.js;
         }
      }
      if (js == null) {
         js = compiler.compile(config.getId(), request);
         List<File> dependencies = getDependencies(config);

         // cache result
         cachedJS = new CachedJS(js, dependencies);
         cache.put(file.relativePath(), cachedJS);
      }
      return js;
   }

   @Override
   public boolean rawInvocation(Request request, Response response) throws Exception {
      // if configuration contains plovr.conf and this file is a plovr-emitted filename
      // plovr emits virtual filenames of the form /input/id/path in RAW mode
      if (inputConfigs != null && request.url.startsWith("/input/") && (request.url.endsWith(".js") || request.url.endsWith(".soy"))) {
         try {
            play.Logger.trace("play-plovr rawInvocation="+request.url);
            int start = "/input/".length();
            String id = request.url.substring(start, request.url.indexOf('/', start));
            String js = fetcher.fetch(id, request);
            response.contentType = "text/javascript";
            response.status = 200;
            if(Play.mode == Play.Mode.PROD) {
               response.cacheFor(cacheFor);
            }
            response.print(js);
         } catch(Exception e) {
            response.contentType = "text/javascript";
            response.status = 500;
            response.print("Oops,\n");
            e.printStackTrace(new PrintStream(response.out));
         }
         return true;
      }
      return super.rawInvocation(request, response);
   }

   private List<File> getDependencies(Config config) {
      List<File> dependencies = new ArrayList<File>();
      for (File f : config.getManifest().getDependencies()) {
         addDependencies(f, dependencies);
      }
      return dependencies;
   }

   private void addDependencies(File f, List<File> dependencies) {
      if (f.isFile()) {
         String fileName = f.getName();
         if (fileName.endsWith(".js") || fileName.endsWith(".soy")) {
            dependencies.add(f);
         }
      }
      else if (f.isDirectory()) {
         for (File entry : f.listFiles()) {
            addDependencies(entry, dependencies);
         }
      }
   }

   Map<String, CachedJS> cache = new HashMap<String, CachedJS>();

   static class CachedJS {
      List<File> deps;
      long ts;
      String js;

      public CachedJS(String js, List<File> deps) {
         this.js = js;
         this.ts = System.currentTimeMillis();
         this.deps = deps;
      }

      public boolean isStillValid() {
         for (File f : deps) {
            if (f.lastModified() > ts) {
               return false;
            }
         }
         return true;
      }
   }
}
